const { WebSocketServer } = require("ws");
const jwt = require("jsonwebtoken");
const qs = require("query-string");

const wss = new WebSocketServer({ port: process.env.port || 3001 });

/**
 *  {
 *      userId: "",
 *      nickName: "",
 *      changed: false,
 *      action: "idle", run | attack | idle
 *      pos: { x: 0, y: 0, z: 0 },
 *      euler: { x: 0, y: 0, z: 0 },
 *  }
 */
const players = [];

const broadcast = (cmd, data) => {
    wss.clients.forEach((ws) => {
        ws.send(`${MESSAGE_TYPE.MESSAGE}${JSON.stringify({ cmd, data })}`);
    });
};

const notify = (ws, cmd, data) => {
    ws.send(`${MESSAGE_TYPE.MESSAGE}${JSON.stringify({ cmd, data })}`);
};

const MESSAGE_TYPE = {
    /**
     * 正常消息
     */
    MESSAGE: "0",
    /**
     * 第一次链接到服务器
     */
    CONNECTED: "1",
    /**
     * 心跳ping
     */
    PING: "2",
    /**
     * 心跳pong
     */
    PONG: "3",
};

const handleMessage = (str, ws) => {
    try {
        const { cmd, token, data } = JSON.parse(str.substring(1));
        const { userId, nickName } = jwt.verify(token, "3d-meta");
        switch (cmd) {
            case "game::enter":
                // data => {pos, euler}
                notify(ws, "game::enter", players);
                break;
            case "player::create":
                // data => {pos, euler}
                {
                    const index = players.findIndex((item) => item.userId === userId);
                    if (index > -1) return;
                    players.push({ userId, nickName, changed: false, ...data });
                    broadcast(cmd, { userId, ...data });
                }
                break;
            case "player::sync":
                // data => {pos, euler}
                const player = players.find((item) => item.userId === userId);
                if (player) {
                    Object.assign(player, data, { changed: true });
                } else {
                    players.push({ userId, nickName, changed: false, ...data });
                    broadcast("player::create", { userId, ...data });
                }
                break;
            case "player::offline":
                {
                    const index = players.findIndex((item) => item.userId === userId);
                    players.splice(index, 1);
                    broadcast(cmd, { userId });
                }
                break;
            case "talk":
                // data => {msg}
                broadcast(cmd, { userId, msg: data.msg });
                break;
        }
    } catch {
        ws.send(str);
    }
};

const checkHeartBeat = (ws) => {
    clearTimeout(ws.heartbeatTimer);

    ws.heartbeatTimer = setTimeout(() => {
        ws.close();
    }, 10 * 1000);
};

wss.on("connection", function connection(ws, req) {
    const { query } = qs.parseUrl(req.url, "token");
    try {
        const payload = jwt.verify(query.token, "3d-meta");
        console.log(payload);

        ws.id = payload.userId;
        ws.send(MESSAGE_TYPE.CONNECTED);
        checkHeartBeat(ws);
    } catch (error) {
        console.log(error);
        ws.close();
    }

    ws.on("message", function message(data) {
        const str = data.toString();
        console.log("received: %s", str);
        const type = str.substring(0, 1);
        switch (type) {
            case MESSAGE_TYPE.MESSAGE:
                handleMessage(str, ws);
                break;
            case MESSAGE_TYPE.PING:
                checkHeartBeat(ws);
                ws.send(MESSAGE_TYPE.PONG);
                break;
        }
    });
});

setInterval(() => {
    if (players.some((item) => item.changed) && wss.clients.size) {
        players.forEach((item) => (item.changed = false));
        broadcast("player::sync", players);
    }
}, 1000 / 60);

console.log(`room server listen at ${process.env.port}.`);
